package animation;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

/**
 * An instance of an animation state machine.
 */
public class AnimationStateMachineInstance {
	public AnimationStateMachineInstance(AnimationStateMachine animationStateMachine, AnimationTreeInstance animationTree) {
		if (animationStateMachine == null || animationTree == null) {
			throw new IllegalArgumentException("Animation state machine or animation tree not specified");
		}
		this.animationStateMachine = animationStateMachine;
		this.animationTree = animationTree;

		commandSets = new ArrayList<CommandSet>();
		states = new HashMap<String, Integer>();
		stateList = new ArrayList<Integer>();
		events = new HashMap<String, Integer>();

		// init count of variables
		floatVariableMap = new HashMap<String, Integer>();
		intVariableMap = new HashMap<String, Integer>();

		Set<String> stateNames = animationStateMachine.getStates();
		Set<String> eventNames = animationStateMachine.getEvents();

		for (String name : stateNames) {
			AnimationStateMachine.CommandSet cs = animationStateMachine.getState(name);
			CommandSet commandSet = new CommandSet(cs, states.size());
			stateList.add(states.size());
			states.put(name, commandSets.size());
			commandSets.add(commandSet);
		}
		for (String name : eventNames) {
			AnimationStateMachine.CommandSet cs = animationStateMachine.getEvent(name);
			CommandSet commandSet = new CommandSet(cs);
			events.put(name, commandSets.size());
			commandSets.add(commandSet);
		}

		// process each command set
		for (CommandSet commandSet : commandSets) {
			processCommandSet(commandSet);
		}
	}

	private void processCommandSet(CommandSet commandSet) {
		for (int i = 0; i < commandSet.getCommandSet().getCommandCount(); ++i) {
			AnimationStateMachine.Command cmd = commandSet.getCommandSet().getCommand(i);
			switch (cmd.getType()) {
				case ANIMATION_PLAY:
					commandSet.addCommand(new AnimationPlayCommand((AnimationStateMachine.AnimationPlayCommand) cmd, this));
					break;
				case ANIMATION_SET_TIME:
					commandSet.addCommand(new AnimationSetTimeCommand((AnimationStateMachine.AnimationSetTimeCommand) cmd, this));
					break;
				case SELECT_SET_INDEX:
					commandSet.addCommand(new SelectSetIndexCommand((AnimationStateMachine.SelectSetIndexCommand) cmd, this));
					break;
				case BLEND_TRANSITION_WEIGHT:
					commandSet.addCommand(new BlendTransitionWeightCommand((AnimationStateMachine.BlendTransitionWeightCommand) cmd, this));
					break;
				case BLEND_SET_WEIGHT:
					commandSet.addCommand(new BlendSetWeightCommand((AnimationStateMachine.BlendSetWeightCommand) cmd, this));
					break;
				case SEQUENCE_TRANSITION_WEIGHT:
					commandSet.addCommand(new SequenceTransitionWeightCommand((AnimationStateMachine.SequenceTransitionWeightCommand) cmd, this));
					break;
				case SEQUENCE_SET_WEIGHT:
					commandSet.addCommand(new SequenceSetWeightCommand((AnimationStateMachine.SequenceSetWeightCommand) cmd, this));
					break;
				case STATE_SET_ACTIVE:
					commandSet.addCommand(new StateSetActiveCommand((AnimationStateMachine.StateSetActiveCommand) cmd, this));
					break;
				case EVENT_TRIGGER:
					commandSet.addCommand(new EventTriggerCommand((AnimationStateMachine.EventTriggerCommand) cmd, this));
					break;
				default:
					throw new IllegalArgumentException("Unknown command type " + cmd.getType().toString());
			}
		}
	}

	private int addFloatVariable(String name) {
		Integer index = floatVariableMap.get(name);
		if (index != null) {
			return index;
		} else {
			int newIndex = floatVariableMap.size();
			floatVariableMap.put(name, newIndex);
			return newIndex;
		}
	}

	private int addIntVariable(String name) {
		Integer index = intVariableMap.get(name);
		if (index != null) {
			return index;
		} else {
			int newIndex = intVariableMap.size();
			intVariableMap.put(name, newIndex);
			return newIndex;
		}
	}

	public static abstract class Command {
		protected Command(AnimationStateMachine.Command command) {
			this.command = command;
		}

		public AnimationStateMachine.Command getCommand() {
			return command;
		}

		private AnimationStateMachine.Command command;

		protected abstract void execute(State state);
	}

	public static class AnimationPlayCommand extends Command {
		private AnimationPlayCommand(AnimationStateMachine.AnimationPlayCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.ANIMATION) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getSpeed().isBoundToVariable()) {
				speedBindingIndex = asmi.addFloatVariable(command.getSpeed().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getSpeedBindingIndex() {
			return speedBindingIndex;
		}

		private int nodeIndex;
		private int speedBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.AnimationPlayCommand command = (AnimationStateMachine.AnimationPlayCommand) getCommand();
			AnimationTreeInstance.State.AnimationNodeState ns = (AnimationTreeInstance.State.AnimationNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setPlaySpeed(command.getSpeed().isBoundToVariable() ?
					state.floatVariables[speedBindingIndex] : command.getSpeed().getConstant());
			ns.setLoop(command.getLoop());
		}
	}

	public static class AnimationSetTimeCommand extends Command {
		private AnimationSetTimeCommand(AnimationStateMachine.AnimationSetTimeCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.ANIMATION) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getTime().isBoundToVariable()) {
				timeBindingIndex = asmi.addFloatVariable(command.getTime().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getTimeBindingIndex() {
			return timeBindingIndex;
		}

		private int nodeIndex;
		private int timeBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.AnimationSetTimeCommand command = (AnimationStateMachine.AnimationSetTimeCommand) getCommand();
			AnimationTreeInstance.State.AnimationNodeState ns = (AnimationTreeInstance.State.AnimationNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setTime(command.getTime().isBoundToVariable() ?
					state.floatVariables[timeBindingIndex] : command.getTime().getConstant());
		}
	}

	public static class SelectSetIndexCommand extends Command {
		private SelectSetIndexCommand(AnimationStateMachine.SelectSetIndexCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.SELECT) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getIndex().isBoundToVariable()) {
				indexBindingIndex = asmi.addIntVariable(command.getIndex().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getIndexBindingIndex() {
			return indexBindingIndex;
		}

		private int nodeIndex;
		private int indexBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.SelectSetIndexCommand command = (AnimationStateMachine.SelectSetIndexCommand) getCommand();
			AnimationTreeInstance.State.SelectNodeState ns = (AnimationTreeInstance.State.SelectNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setIndex(command.getIndex().isBoundToVariable() ?
					state.intVariables[indexBindingIndex] : command.getIndex().getConstant());
		}
	}

	public static class BlendTransitionWeightCommand extends Command {
		private BlendTransitionWeightCommand(AnimationStateMachine.BlendTransitionWeightCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.BLEND) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getWeight().isBoundToVariable()) {
				weightBindingIndex = asmi.addFloatVariable(command.getWeight().getVariable());
			}
			if (command.getSpeed().isBoundToVariable()) {
				speedBindingIndex = asmi.addFloatVariable(command.getSpeed().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getWeightBindingIndex() {
			return weightBindingIndex;
		}

		public int getSpeedBindingIndex() {
			return speedBindingIndex;
		}

		private int nodeIndex;
		private int weightBindingIndex;
		private int speedBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.BlendTransitionWeightCommand command = (AnimationStateMachine.BlendTransitionWeightCommand) getCommand();
			AnimationTreeInstance.State.BlendNodeState ns = (AnimationTreeInstance.State.BlendNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setTargetWeight(command.getWeight().isBoundToVariable() ?
					state.floatVariables[weightBindingIndex] : command.getWeight().getConstant());
			ns.setTransitionSpeed(command.getSpeed().isBoundToVariable() ?
					state.floatVariables[speedBindingIndex] : command.getSpeed().getConstant());
		}
	}

	public static class BlendSetWeightCommand extends Command {
		private BlendSetWeightCommand(AnimationStateMachine.BlendSetWeightCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.BLEND) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getWeight().isBoundToVariable()) {
				weightBindingIndex = asmi.addFloatVariable(command.getWeight().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getWeightBindingIndex() {
			return weightBindingIndex;
		}

		private int nodeIndex;
		private int weightBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.BlendSetWeightCommand command = (AnimationStateMachine.BlendSetWeightCommand) getCommand();
			AnimationTreeInstance.State.BlendNodeState ns = (AnimationTreeInstance.State.BlendNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setWeight(command.getWeight().isBoundToVariable() ?
					state.floatVariables[weightBindingIndex] : command.getWeight().getConstant());
		}
	}

	public static class SequenceTransitionWeightCommand extends Command {
		private SequenceTransitionWeightCommand(AnimationStateMachine.SequenceTransitionWeightCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.SEQUENCE) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getWeight().isBoundToVariable()) {
				weightBindingIndex = asmi.addFloatVariable(command.getWeight().getVariable());
			}
			if (command.getSpeed().isBoundToVariable()) {
				speedBindingIndex = asmi.addFloatVariable(command.getSpeed().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getWeightBindingIndex() {
			return weightBindingIndex;
		}

		public int getSpeedBindingIndex() {
			return speedBindingIndex;
		}

		private int nodeIndex;
		private int weightBindingIndex;
		private int speedBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.SequenceTransitionWeightCommand command = (AnimationStateMachine.SequenceTransitionWeightCommand) getCommand();
			AnimationTreeInstance.State.SequenceNodeState ns = (AnimationTreeInstance.State.SequenceNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setTargetWeight(command.getIndex(), command.getWeight().isBoundToVariable() ?
					state.floatVariables[weightBindingIndex] : command.getWeight().getConstant());
			ns.setTransitionSpeed(command.getIndex(), command.getSpeed().isBoundToVariable() ?
					state.floatVariables[speedBindingIndex] : command.getSpeed().getConstant());
		}
	}

	public static class SequenceSetWeightCommand extends Command {
		private SequenceSetWeightCommand(AnimationStateMachine.SequenceSetWeightCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			nodeIndex = asmi.animationTree.getNodeIndex(command.getNodeName());
			if (nodeIndex < 0) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " not found");
			}
			if (asmi.animationTree.getNode(nodeIndex).getNode().getType() != AnimationTree.Node.Type.SEQUENCE) {
				throw new IllegalArgumentException("Node " + command.getNodeName() + " is not compatible with " + command.getType() + " command");
			}
			if (command.getWeight().isBoundToVariable()) {
				weightBindingIndex = asmi.addFloatVariable(command.getWeight().getVariable());
			}
		}

		public int getNodeIndex() {
			return nodeIndex;
		}

		public int getWeightBindingIndex() {
			return weightBindingIndex;
		}

		private int nodeIndex;
		private int weightBindingIndex;

		protected void execute(State state) {
			// get command and node
			AnimationStateMachine.SequenceSetWeightCommand command = (AnimationStateMachine.SequenceSetWeightCommand) getCommand();
			AnimationTreeInstance.State.SequenceNodeState ns = (AnimationTreeInstance.State.SequenceNodeState) state.animationTreeState.getNodeState(nodeIndex);

			ns.setWeight(command.getIndex(), command.getWeight().isBoundToVariable() ?
					state.floatVariables[weightBindingIndex] : command.getWeight().getConstant());
		}
	}

	public static class StateSetActiveCommand extends Command {
		private StateSetActiveCommand(AnimationStateMachine.StateSetActiveCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			int index = asmi.getStateCommandSetIndex(command.getStateName());
			if (index < 0) {
				throw new IllegalArgumentException("State " + command.getStateName() + " not found");
			}
			stateIndex = asmi.getCommandSet(index).getStateIndex();
		}

		public int getStateIndex() {
			return stateIndex;
		}

		private int stateIndex;

		protected void execute(State state) {
			// get command and state
			AnimationStateMachine.StateSetActiveCommand command = (AnimationStateMachine.StateSetActiveCommand) getCommand();
			state.activeStates[stateIndex] = command.getActive();
		}
	}

	public static class EventTriggerCommand extends Command {
		private EventTriggerCommand(AnimationStateMachine.EventTriggerCommand command, AnimationStateMachineInstance asmi) {
			super(command);
			eventIndex = asmi.getEventCommandSetIndex(command.getEventName());
			if (eventIndex < 0) {
				throw new IllegalArgumentException("Event " + command.getEventName() + " not found");
			}
		}

		public int getEventIndex() {
			return eventIndex;
		}

		private int eventIndex;

		protected void execute(State state) {
			// trigger the event
			state.executeCommandSet(eventIndex);
		}
	}

	public static class CommandSet {
		private CommandSet(AnimationStateMachine.CommandSet commandSet) {
			this.commandSet = commandSet;
			commands = new ArrayList<Command>();
			stateIndex = -1;
		}

		private CommandSet(AnimationStateMachine.CommandSet commandSet, int stateIndex) {
			this(commandSet);
			this.stateIndex = stateIndex;
		}

		private void addCommand(Command command) {
			commands.add(command);
		}

		public AnimationStateMachine.CommandSet getCommandSet() {
			return commandSet;
		}

		public Command getCommand(int i) {
			return commands.get(i);
		}

		private int getStateIndex() {
			return stateIndex;
		}

		private AnimationStateMachine.CommandSet commandSet;
		private ArrayList<Command> commands;
		private int stateIndex;
	}

	public AnimationStateMachine getAnimationStateMachine() {
		return animationStateMachine;
	}

	public AnimationTreeInstance getAnimationTree() {
		return animationTree;
	}

	public CommandSet getCommandSet(int index) {
		return commandSets.get(index);
	}

	public int getStateCommandSetIndex(String stateName) {
		Integer index = states.get(stateName);
		return (index == null) ? -1 : index;
	}

	public int getEventCommandSetIndex(String eventName) {
		Integer index = events.get(eventName);
		return (index == null) ? -1 : index;
	}

	private AnimationStateMachine animationStateMachine;
	private AnimationTreeInstance animationTree;

	private ArrayList<CommandSet> commandSets;
	private HashMap<String, Integer> states; // maps to command set index
	private ArrayList<Integer> stateList;    // maps state index to command index so that states can be accessed by ID
	private HashMap<String, Integer> events; // maps to command set index

	private HashMap<String, Integer> floatVariableMap; // maps float variables to indices
	private HashMap<String, Integer> intVariableMap;   // maps int variables to indices

	////////
	// state

	public State createState() {
		return new State(this);
	}

	public static class State {
		private State(AnimationStateMachineInstance instance) {
			this.instance = instance;
			animationTreeState = instance.animationTree.createState();
			floatVariables = new float[instance.floatVariableMap.size()];
			intVariables = new int[instance.intVariableMap.size()];
			activeStates = new boolean[instance.states.size()];
		}

		public AnimationStateMachineInstance getAnimationStateMachineInstance() {
			return instance;
		}

		public AnimationTreeInstance.State getAnimationTreeState() {
			return animationTreeState;
		}

		public void setVariable(String name, float value) {
			Integer index = instance.floatVariableMap.get(name);
			if (index == null) {
				throw new IllegalArgumentException("Float variable " + name + " does not exist");
			}
			floatVariables[index] = value;
		}

		public void setVariable(String name, int value) {
			Integer index = instance.intVariableMap.get(name);
			if (index == null) {
				throw new IllegalArgumentException("Int variable " + name + " does not exist");
			}
			intVariables[index] = value;
		}

		public void setStateActive(String name, boolean active) {
			int index = instance.getStateCommandSetIndex(name);
			if (index < 0) {
				throw new IllegalArgumentException("State " + name + " does not exist");
			}
			activeStates[instance.getCommandSet(index).getStateIndex()] = active;
		}

		public boolean isStateActive(String name) {
			int index = instance.getStateCommandSetIndex(name);
			if (index < 0) {
				throw new IllegalArgumentException("State " + name + " does not exist");
			}
			return activeStates[instance.getCommandSet(index).stateIndex];
		}

		public void triggerEvent(String name) {
			int index = instance.getEventCommandSetIndex(name);
			if (index < 0) {
				throw new IllegalArgumentException("Event " + name + " does not exist");
			}
			executeCommandSet(index);
		}

		// executes active states
		public void update() {
			for (int i = 0; i < activeStates.length; ++i) {
				if (activeStates[i]) {
					executeCommandSet(instance.stateList.get(i));
				}
			}
		}

		private void executeCommandSet(int csIdx) {
			CommandSet cs = instance.getCommandSet(csIdx);
			for (int i = 0; i < cs.getCommandSet().getCommandCount(); ++i) {
				Command cmd = cs.getCommand(i);
				cmd.execute(this);
			}
		}

		private AnimationStateMachineInstance instance;
		private AnimationTreeInstance.State animationTreeState;
		private float[] floatVariables;
		private int[] intVariables;
		private boolean[] activeStates;
	}
}
